iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

我是Java工程師,關於密碼學我想懂的不多系列 第 32 篇

Day31 - 數位簽章 (Digital Signature):不可否認性與 Ed25519 / RSA-PSS 實戰

  • 分享至 

  • xImage
  •  

前言

在前面的章節中,我們主要聚焦於資料的「機密性(Confidentiality)」——如何透過 AES-GCM 或 ECIES 混合加密讓竊聽者無法讀取訊息內容。

然而,在零信任與開放網路架構中,單靠機密性是不夠的。我們還面臨三個關鍵問題:

  1. 身份驗證(Authentication):這封訊息真的是聲稱的傳送者發出的嗎?
  2. 資料完整性(Integrity):訊息在傳輸過程中是否被篡改過?
  3. 不可否認性(Non-repudiation):傳送者在發送訊息後,事後能否抵賴、否認該行為?

對稱加密與 MAC 雖然能提供完整性與驗證,但因為金鑰為雙方共享,無法區分是哪一方產生的訊息,因此無法提供「不可否認性」。

為解決這個難題,數位簽章(Digital Signature) 應運而生。本章將解析數位簽章的原理,剖析歷史上因簽章漏洞發生的資安浩劫(Sony PS3 事件),並透過 Java JCA 帶領讀者實戰現代兩大主流簽章標準——Ed25519 與 RSA-PSS。

一、什麼是數位簽章 (Digital Signature)?

在實體世界中,我們靠「親筆簽名」或「蓋章」來代表對文件的認可與法律效力;但在數位世界中,一張複製貼上的簽名圖檔(PNG)任何人都能輕易偽造,完全無法提供安全性。

數位簽章(Digital Signature) 是現代密碼學的基石之一。它是基於非對稱密碼學(Asymmetric Cryptography)與密碼雜湊函數(Cryptographic Hash Function)結合產生的數學證明,能將「簽署者的身份」與「特定的數位資料內容」進行不可分割的技術綁定。

一個標準的數位簽章機制必須同時滿足以下三大安全屬性:

  1. 身份認證 (Authenticity):驗證端能確信該訊息確實來自持有特定私鑰的簽署者。
  2. 資料完整性 (Integrity):確保訊息在傳輸過程中未遭攻擊者篡改或毀損(哪怕只動了 1 個 bit,驗證也會失敗)。
  3. 不可否認性 (Non-repudiation):簽署者事後無法否認自己曾簽署該訊息,因為該簽章只能由其私密持有且不可導出的私鑰生成。

二、數位簽章的運作原理 (Signing & Verification)

數位簽章並非直接對龐大的原始文件(如 100MB 的 PDF 或合約)進行私鑰加密,這樣做在效能上極度低效。現代數位簽章採用「先雜湊,再簽署」的標準流程:

【簽署階段 (Signing Phase)】— 簽署者 Alice 持有私鑰 (PrivateKey)
┌─────────────┐       ┌──────────────┐       ┌─────────────────┐
│ 原始訊息    │ ───> │ 密碼雜湊函數 │ ───> │ 訊息摘要        │
│ (Message)   │       │ (SHA-256/3)  │       │ (Message Hash)  │
└─────────────┘       └──────────────┘       └─────────────────┘
                                                      │
                                                      ▼
                                             ┌─────────────────┐
                                             │ 用 Alice 私鑰簽署│ (Sign Algorithm)
                                             └─────────────────┘
                                                      │
                                                      ▼
                                             ┌─────────────────┐
                                             │ 數位簽章位元組  │ (Signature)
                                             └─────────────────┘

--------------------------------------------------------------------------------

【驗證階段 (Verification Phase)】— 驗證者 Bob 持有 Alice 的公鑰 (PublicKey)
┌─────────────┐       ┌──────────────┐       ┌─────────────────┐
│ 收到訊息    │ ───> │ 密碼雜湊函數 │ ───> │ 計算出來的 Hash │ ──┐
│ (Message)   │       │ (SHA-256/3)  │       │ (Hash A)        │   │
└─────────────┘       └──────────────┘       └─────────────────┘   │
                                                                           ├─ 相等?(Valid/Invalid)
┌─────────────┐       ┌──────────────┐       ┌─────────────────┐   │
│ 收到簽章    │ ───> │ 用 Alice 公鑰│ ───> │ 還原出的 Hash   │ ──┘
│ (Signature) │       │ 執行簽章驗算 │       │ (Hash B)        │
└─────────────┘       └──────────────┘       └─────────────────┘

  1. 簽署過程(Sign):
  • 簽署者將原始訊息丟入雜湊函數(如 SHA-256),算出一段固定長度的訊息摘要(Digest / Hash)。
  • 簽署者使用自己的私鑰(Private Key),對該訊息摘要進行簽章演算法運算,產出數位簽章(Signature)。
  • 將「原始訊息 + 數位簽章」一同發送給接收方。
  1. 驗證過程(Verify):
  • 驗證者收到訊息後,自行重新計算一次訊息的 SHA-256 摘要(Hash A)。
  • 驗證者使用簽署者的公鑰(Public Key)與簽章,進行解密或數學校驗,還原出簽署當下的摘要(Hash B)。
  • 比對 Hash A 是否等於 Hash B:
  • 若完全相同:證明訊息未被篡改,且確實由該公鑰對應的私鑰持有者簽署。
  • 若不相同:代表訊息遭篡改、或簽章係偽造。

三、 RSA 簽章演進:從 PKCS#1 v1.5 到 RSA-PSS

RSA 簽章演算法經歷了顯著的安全演進:

1. 舊型 PKCS#1 v1.5 簽章的致命缺陷

傳統的 RSA PKCS#1 v1.5 簽章 是確定性的(Deterministic)。若實現不當(如未嚴格檢查 Padding 結構),容易遭受 Bleichenbacher 簽章偽造攻擊 或 Fault Attack。現代資安規範(如 NIST SP 800-56B)已強烈建議停止在新型系統中使用 PKCS#1 v1.5 簽章。

2. 現代黃金標準:RSA-PSS (Probabilistic Signature Scheme)

RSA-PSS(RFC 8017 / PKCS#1 v2.2) 引入了隨機鹽值(Salt)與 MGF1 遮蔽函數,將確定性簽章轉化為機率性簽章(Probabilistic Signature):

[ Message Hash ] ──> [ Padding & Salt 遮蔽 (MGF1) ] ──> [ EM ] ──> [ RSA 私鑰指數運算 (s = EMᵈ mod n) ]

  • 全域隨機化:即便對同一份檔案簽署 100 次,產出的簽章值也完全不同。
  • 可證明安全性(Provable Security):在隨機預言機模型(Random Oracle Model)下,RSA-PSS 被數學證明嚴格等價於大整數分解難題(IFP)。

四、 ECC 簽章演進:ECDSA 的 k-nonce 陷阱與 Ed25519 的崛起

1. 歷史級資安浩劫:Sony PS3 主機私鑰洩漏事件

傳統橢圓曲線簽章 ECDSA 在數學上非常安全,但它隱藏著一個極度危險的工程陷阱——簽章隨機數(k-nonce)的重用。

在 ECDSA 演算法中,每次簽章都必須透過 SecureRandom 產生一個一次性的純量 k:

r = (k * G)_x (mod n)

s ≡ k^{-1} (z + r * d) (mod n) \quad (d \text{ 為私鑰}, z \text{ 為訊息雜湊})

2010 年,Sony 在 PS3 遊戲主機的韌體簽章系統中犯下了低級錯誤:硬編碼了固定的 k 值。

攻擊者只要拿到同一把私鑰簽署的兩份不同訊息 (m_1, r, s_1) 與 (m_2, r, s_2),透過基礎代數即可直接還原出私鑰 d:

s_1 - s_2 ≡ k^{-1}(z_1 - z_2) (mod n) => k ≡ \frac{z_1 - z_2}{s_1 - s_2} (mod n)

求得 k 後代回公式:

d ≡ \frac{s_1 * k - z_1}{r} (mod n)

這導致 PS3 的根私鑰完全洩漏,防禦機制徹底崩潰。

💡 補救方案:RFC 6979 提出了「確定性 ECDSA」,改用 HMAC-SHA256(私鑰, 訊息) 動態衍生 k,從根本上解決了隨機數發生器(RNG)失效的問題。

2. 新一代簽章王者:Ed25519 (EdDSA)

為了徹底解決 ECDSA 的複雜性與側信道威脅,密碼學家 Daniel J. Bernstein 等人提出了 Ed25519(Edwards-curve Digital Signature Algorithm,RFC 8032)。

Ed25519 建立在 Curve25519 / Edwards25519 扭曲愛德華曲線上,具備以下革命性優勢:

  1. 天然免疫 k-nonce 洩漏:Ed25519 內部自動使用私鑰與訊息的 Hash 衍生 nonce,不依賴外部隨機數發生器,天然具備 RFC 6979 特性。
  2. 常數時間執行(Constant-Time):所有點運算均不包含條件分支與動態記憶體查表,徹底免疫時序側信道攻擊(Timing Side-Channel Attacks)。
  3. 極致極速與小尺寸:
  • 私鑰尺寸:32 Bytes
  • 公鑰尺寸:32 Bytes
  • 簽章尺寸:64 Bytes
  • 簽驗簽速度比傳統 RSA-3072 快數十倍,且支援高併發批次驗簽(Batch Verification)。

五、 Java JCA 生產級實戰:Ed25519 與 RSA-PSS 簽驗簽

Java 15+ 原生引入了對 Ed25519 的標準 JCA 支援(Ed25519),同時 Java 也完善支援了 RSASSA-PSS。以下範例演示如何在 Java 中實作這兩種現代簽章標準:

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.Base64;

public class DigitalSignatureProductionExample {

    public static void main(String[] args) throws Exception {
        System.out.println("=== Day 31 - 現代數位簽章 (Ed25519 與 RSA-PSS) 實戰 ===");

        String document = "Action: Transfer 100,000 to Account-8899. Non-repudiable audit log.";
        byte[] dataBytes = document.getBytes(StandardCharsets.UTF_8);

        // ==========================================
        // 實戰 1: Ed25519 簽章與驗簽 (現代推薦首選)
        // ==========================================
        System.out.println("\n--- [ 模式一:Ed25519 (EdDSA) 數位簽章 ] ---");

        // 1. 生成 Ed25519 KeyPair (Java 15+)
        KeyPairGenerator ed25519KeyGen = KeyPairGenerator.getInstance("Ed25519");
        KeyPair edKeyPair = ed25519KeyGen.generateKeyPair();

        // 2. 簽署階段 (Sign)
        byte[] edSignatureBytes = signEd25519(dataBytes, edKeyPair.getPrivate());
        System.out.println("Ed25519 公鑰長度: " + edKeyPair.getPublic().getEncoded().length + " bytes");
        System.out.println("Ed25519 簽章長度: " + edSignatureBytes.length + " bytes (固定 64 bytes)");
        System.out.println("簽章 Base64: " + Base64.getEncoder().encodeToString(edSignatureBytes));

        // 3. 驗簽階段 (Verify)
        boolean isEdValid = verifyEd25519(dataBytes, edSignatureBytes, edKeyPair.getPublic());
        System.out.println("Ed25519 簽章驗證結果: " + isEdValid);

        // ==========================================
        // 實戰 2: RSA-PSS 簽章與驗簽 (傳統相容首選)
        // ==========================================
        System.out.println("\n--- [ 模式二:RSA-PSS (RSASSA-PSS) 數位簽章 ] ---");

        // 1. 生成 3072-bit RSA KeyPair
        KeyPairGenerator rsaKeyGen = KeyPairGenerator.getInstance("RSA");
        rsaKeyGen.initialize(3072);
        KeyPair rsaKeyPair = rsaKeyGen.generateKeyPair();

        // 2. 簽署階段 (Sign)
        byte[] pssSignatureBytes = signRsaPss(dataBytes, rsaKeyPair.getPrivate());
        System.out.println("RSA-3072 PSS 簽章長度: " + pssSignatureBytes.length + " bytes");

        // 3. 驗驗階段 (Verify)
        boolean isPssValid = verifyRsaPss(dataBytes, pssSignatureBytes, rsaKeyPair.getPublic());
        System.out.println("RSA-PSS 簽章驗證結果: " + isPssValid);
    }

    /**
     * Ed25519 私鑰簽署
     */
    public static byte[] signEd25519(byte[] data, PrivateKey privateKey) throws Exception {
        Signature signer = Signature.getInstance("Ed25519");
        signer.initSign(privateKey);
        signer.update(data);
        return signer.sign();
    }

    /**
     * Ed25519 公鑰驗簽
     */
    public static boolean verifyEd25519(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
        Signature verifier = Signature.getInstance("Ed25519");
        verifier.initVerify(publicKey);
        verifier.update(data);
        return verifier.verify(signature);
    }

    /**
     * RSA-PSS 私鑰簽署 (指定 SHA-256 與 MGF1-SHA256)
     */
    public static byte[] signRsaPss(byte[] data, PrivateKey privateKey) throws Exception {
        Signature signer = Signature.getInstance("RSASSA-PSS");

        // 配置 PSS 參數:Primary Hash = SHA-256, MGF1 Hash = SHA-256, Salt Length = 32 bytes
        PSSParameterSpec pssSpec = new PSSParameterSpec(
                "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1
        );
        signer.setParameter(pssSpec);

        signer.initSign(privateKey);
        signer.update(data);
        return signer.sign();
    }

    /**
     * RSA-PSS 公鑰驗簽
     */
    public static boolean verifyRsaPss(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
        Signature verifier = Signature.getInstance("RSASSA-PSS");

        PSSParameterSpec pssSpec = new PSSParameterSpec(
                "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1
        );
        verifier.setParameter(pssSpec);

        verifier.initVerify(publicKey);
        verifier.update(data);
        return verifier.verify(signature);
    }
}


上一篇
Day30 - 金鑰交換協定:Diffie-Hellman (DH) 與 ECDH 金鑰協商實務
下一篇
Day32 - 數位憑證與公鑰基礎設施 (PKI)
系列文
我是Java工程師,關於密碼學我想懂的不多 共 38 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言